using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Serialization;
using System.Threading;
using System.Web.Caching;
using Aricie.DNN.Caching.Analysis;
using Aricie.Services;
using Redlock.CSharp;
using StackExchange.Redis;

namespace Aricie.DNN.Caching.Client.RedisCachingProvider
{
    public class RedisClientProvider : CachingClientProvider<CachingPortBasedHostInfo, RedisClusterSettings>
    {
        private ConfigurationOptions _Configuration;
        private ConnectionMultiplexer _Redis;
        
        private Redlock.CSharp.Redlock _Redlock;


        public ConfigurationOptions Configuration
        {
            get
            {
                if (_Configuration == null)
                {
                    ConfigurationOptions config = new ConfigurationOptions();
                    foreach (var cachingClientHostInfo in this.ClusterSettings.Hosts)
                    {
                        var objHostInfo = (CachingPortBasedHostInfo)cachingClientHostInfo;
                        config.EndPoints.Add(objHostInfo.Name, objHostInfo.CachePort);
                    }

                    if (!this.SpecificClusterSettings.ClientName.IsNullOrEmpty())
                    {
                        config.ClientName = SpecificClusterSettings.ClientName;
                    }
                    if (!this.SpecificClusterSettings.Password.IsNullOrEmpty())
                    {
                        config.Password = SpecificClusterSettings.Password;
                    }
                    config.KeepAlive = SpecificClusterSettings.KeepAlive;
                    config.SyncTimeout = SpecificClusterSettings.SyncTimeout;
                    config.TieBreaker = SpecificClusterSettings.TieBreaker;
                    config.WriteBuffer = SpecificClusterSettings.WriteBuffer;

                    config.Ssl = SpecificClusterSettings.UseSsl;
                    if (!this.SpecificClusterSettings.SslHost.IsNullOrEmpty())
                    {
                        config.SslHost = SpecificClusterSettings.SslHost;
                    }
                    config.AllowAdmin = SpecificClusterSettings.AllowAdmin;
                    config.AbortOnConnectFail = SpecificClusterSettings.AbortOnConnectFail;


                    config.DefaultVersion = SpecificClusterSettings.RedisVersion;
                    config.CommandMap = CommandMap.Create(SpecificClusterSettings.CommandMap);


                    if (!this.SpecificClusterSettings.ConfigurationChannel.IsNullOrEmpty())
                    {
                        config.ConfigurationChannel = SpecificClusterSettings.ConfigurationChannel;
                    }
                    if (!this.SpecificClusterSettings.ChannelPrefix.IsNullOrEmpty())
                    {
                        config.ChannelPrefix = SpecificClusterSettings.ChannelPrefix;
                    }
                    config.ResolveDns = SpecificClusterSettings.ResolveDns;
                    if (!this.SpecificClusterSettings.ServiceName.IsNullOrEmpty())
                    {
                        config.ServiceName = SpecificClusterSettings.ServiceName;
                    }
                    config.Proxy = SpecificClusterSettings.Proxy;
                    _Configuration= config;
                    if (DistributedCachingProviderBase.Settings.LoggingSettings.EnableStatsCollector)
                    {
                        KeyValuePair<string, string> nbEndPointsProp = new KeyValuePair<string, string>(CacheStepProperty.NbEndPoints, Configuration.EndPoints.Count.ToString(CultureInfo.InvariantCulture));
                        DistributedCachingBusinessController.LogCachingOperation(CacheTimings.Init, "Redis Config prepared", Aricie.DNN.Diagnostics.WorkingPhase.InProgress, false, "", true, nbEndPointsProp);
                    }
                }
                return _Configuration;
            }
           
        }

        public ConnectionMultiplexer Redis
        {
            get
            {
                if (_Redis == null)
                {
                    if (this.ClusterSettings.Hosts.Count > 0)
                    {
                       
                        _Redis = ConnectionMultiplexer.Connect(Configuration);

                        if (DistributedCachingProviderBase.Settings.LoggingSettings.EnableStatsCollector)
                        {
                            DistributedCachingBusinessController.LogCachingOperation(CacheTimings.Init, "Redis Multiplexer created", Aricie.DNN.Diagnostics.WorkingPhase.EndOverhead, false, "", true);
                        }
                    }
                    else
                    {
                        throw new ApplicationException("DistributedCaching Engine was enabled with no DistributedCaching Host Configured, please review configuration");
                    }
                }
                return _Redis;
            }
        }

        public Redlock.CSharp.Redlock Redlock
        {
            get
            {
                if (_Redlock == null)
                {
                    var cmps = new List<ConnectionMultiplexer>();
                    foreach (var objEndPoint in Configuration.EndPoints)
                    {
                        var configClone = Configuration.Clone();
                        configClone.EndPoints.Clear();
                        configClone.EndPoints.Add(objEndPoint);
                        cmps.Add(ConnectionMultiplexer.Connect(configClone));
                    }
                    _Redlock = new Redlock.CSharp.Redlock(cmps.ToArray());
                    
                }
                return _Redlock;
            }
          
        }


        private IDatabase GetDatabase()
        {
            return Redis.GetDatabase(SpecificClusterSettings.DbNumber);
        }

        private NetDataContractSerializer _Serializer = new NetDataContractSerializer();

        private WrappedDistributedCacheObject Deserialize(string strResult)
        {
            WrappedDistributedCacheObject toReturn;
            using (var objStream = strResult.ToStream())
            {
                toReturn = (WrappedDistributedCacheObject)_Serializer.Deserialize(objStream);

            }
            return toReturn; 
        }

        private string Serialize(WrappedDistributedCacheObject value)
        {
            string toReturn;
            using (var objStream = new MemoryStream())
            {
                _Serializer.Serialize(objStream, value);
                toReturn = objStream.FromUtf8();
            }
            return toReturn;
        }

        public override WrappedCacheObject Get(CacheJob objCacheJob)
        {

            string strResult = "";
            try
            {
                var db = GetDatabase();
                //todo: deal with concurrency as in appfabric provider
                strResult = db.StringGet(objCacheJob.FormattedKey);
                if (!strResult.IsNullOrEmpty())
                {
                    //WrappedDistributedCacheObject toReturn = ReflectionHelper.Deserialize<WrappedDistributedCacheObject>(strResult);
                    //WrappedDistributedCacheObject toReturn = Jil.JSON.Deserialize<WrappedDistributedCacheObject>(strResult);
                    return Deserialize(strResult);
                }
            }
            catch (Exception ex)
            {
                string message;
                if (!strResult.IsNullOrEmpty())
                {
                    message = string.Format("Error attempting to retrieve key {0} with value {1}",
                        objCacheJob.FormattedKey, strResult);
                }
                else
                {
                    message = string.Format("Error attempting to retrieve key {0}",objCacheJob.FormattedKey);
                }
                var wrapper =
                    new ApplicationException(message, ex);
                try
                {
                    throw wrapper;
                }
                catch (Exception wrapped)
                {

                    this.ProcessException(wrapped, "Redis Exception", true);
                }
                
            }
            return null;
        }

        public override IEnumerable<KeyValuePair<string, WrappedCacheObject>> MultipleGet(CacheJob objCacheJob)
        {
            try
            {
                IEnumerable<String> objKeys = this.GetTagKeys(objCacheJob);
                var redisKeys = objKeys.Select(strKey => (RedisKey) strKey).ToList();

                var db = GetDatabase();
                //todo: deal with concurrency as in appfabric provider
                RedisValue[] values = db.StringGet(redisKeys.ToArray());
                var toReturn = new Dictionary<string, WrappedCacheObject>(values.Length);
                for (int i = 0; i < redisKeys.Count; i++)
                {
                    RedisValue currentValue = values[i];
                    if (!currentValue.IsNullOrEmpty)
                    {
                        //WrappedDistributedCacheObject currentDeserialized =
                        //    ReflectionHelper.Deserialize<WrappedDistributedCacheObject>(currentValue.ToString());
                        //WrappedDistributedCacheObject currentDeserialized =
                           //Jil.JSON.Deserialize<WrappedDistributedCacheObject>(currentValue.ToString());
                        WrappedDistributedCacheObject currentDeserialized = Deserialize(currentValue.ToString());
                        toReturn.Add(redisKeys[i].ToString(), currentDeserialized);
                    }
                }
                return toReturn;
            }
            catch (Exception ex)
            {
                var wrapper =
                    new ApplicationException(
                        string.Format("Error attempting to retrieve multiple keys from tags with original key {0}", objCacheJob.FormattedKey), ex);
                try
                {
                    throw wrapper;
                }
                catch (Exception wrapped)
                {

                    this.ProcessException(wrapped, "Redis Exception", true);
                }
            }
            return  new Dictionary<string, WrappedCacheObject>();
        }


        public override bool Put(CachePutJob objCacheJob)
        {

            try
            {
                if (objCacheJob.Value == null)
                {
                    throw new ApplicationException("Impossible to distribute null object with key" +
                                                   objCacheJob.Job.FormattedKey);
                }
                var db = GetDatabase();
                //todo: deal with concurrency as in appfabric provider
                //var strValue = ReflectionHelper.Serialize(objCacheJob.Value).Beautify();
                string strValue = Serialize((WrappedDistributedCacheObject)objCacheJob.Value);
                //= Jil.JSON.Serialize<WrappedDistributedCacheObject>((WrappedDistributedCacheObject)objCacheJob.Value);


                var timeOut = objCacheJob.GetTimeOut();
                bool toReturn;
                if (timeOut == TimeSpan.Zero)
                {
                    toReturn = db.StringSet(objCacheJob.Job.FormattedKey, strValue);
                }
                else
                {
                    toReturn = db.StringSet(objCacheJob.Job.FormattedKey, strValue, timeOut);
                }
                return toReturn;
            }
            catch (Exception ex)
            {
                var wrapper =
                  new ApplicationException(
                      string.Format("Error attempting to serialize object with key {0} and object of type {1}", objCacheJob.Job.FormattedKey, objCacheJob.Value.Value.GetType()), ex);
                try
                {
                    throw wrapper;
                }
                catch (Exception wrapped)
                {

                    this.ProcessException(wrapped, "Redis Exception", true);
                }
            }


            return false;
        }

        public static Guid Guid = Guid.NewGuid();

        public override void Remove(CacheJob objJob)
        {
            try
            {
                if (objJob.Strategy.DistributionType != CacheDistributionType.NotDistributed)
                {
                    var db = GetDatabase();
                    db.KeyDelete(objJob.FormattedKey);
                    NotifyRemoval(objJob);
                }
                else if (objJob.Strategy.NotifyRemovals)
                {
                    NotifyRemoval(objJob);
                }
            }
            catch (Exception ex)
            {
                this.ProcessException(ex, "Redis Exception", true);
            }
        }

        public void NotifyRemoval(CacheJob objJob)
        {
            if (SpecificClusterSettings.CallbackMode == RedisCallbackMode.CustomChannel)
            {
                var db = GetDatabase();
                var channelKey = SpecificClusterSettings.CallbackChannel;
                if (SpecificClusterSettings.CallbackChannelIncludeFingerPrint)
                {
                    channelKey += ":" + RedisClientProvider.Guid.ToString();
                }
                db.Publish(channelKey, objJob.Key);
            }
        }


        private RedisCallBackAdapter _CallbackAdapter;

        public override bool AddCallBack(CacheJob objCacheJob, CacheItemRemovedCallback itemRemovedCallback)
        {
            if (_CallbackAdapter == null)
            {
                _CallbackAdapter = new RedisCallBackAdapter(itemRemovedCallback, this);
                try
                {
                    var sub = Redis.GetSubscriber();
                    switch (SpecificClusterSettings.CallbackMode)
                    {
                        case RedisCallbackMode.CustomChannel:
                            if (SpecificClusterSettings.CallbackChannelIncludeFingerPrint)
                            {
                                sub.Subscribe(SpecificClusterSettings.CallbackChannel+":*", _CallbackAdapter.ProcessMessage);    
                            }
                            else
                            {
                                sub.Subscribe(SpecificClusterSettings.CallbackChannel, _CallbackAdapter.ProcessMessage);    
                            }
                            break;
                        case RedisCallbackMode.KeyspaceNotifications:
                            sub.Subscribe(string.Format("__keyevent@{0}__:*", SpecificClusterSettings.DbNumber.ToString(CultureInfo.InvariantCulture)), _CallbackAdapter.ProcessMessage);
                            sub.Subscribe(string.Format("__keyspace@{0}__:*", SpecificClusterSettings.DbNumber.ToString(CultureInfo.InvariantCulture)), _CallbackAdapter.ProcessMessage);
                            break;
                        default:
                            throw new ArgumentOutOfRangeException();
                    }
                    
                    return true;
                }
                catch (Exception)
                {
                    _CallbackAdapter = null;
                    return false;
                }
            }
            _CallbackAdapter.SetCallBack(itemRemovedCallback);
            return true;
        }

       

        public override void ResetTimeOut(CachePutJob objCacheJob)
        {
            try
            {
                if (objCacheJob.Value != null && objCacheJob.Value.GetType() == typeof (WrappedCacheObject))
                {
                    var db = GetDatabase();

                    db.KeyExpire(objCacheJob.Job.FormattedKey, objCacheJob.GetTimeOut());
                }
            }
            catch (Exception ex)
            {
                this.ProcessException(ex, "Redis Exception", true);
            }
        }

        public override void Stop()
        {
            base.Stop();
            this.ReleaseLock();
            _CallbackAdapter = null;
            var currentRedis = _Redis;
            if (currentRedis != null)
            {
                currentRedis.Dispose();
                _Redis = null;
            }
            _Configuration = null;
        }

        public override void CreateRegion(CacheJob objCacheJob)
        {
            //do nothing
        }

        public override void ClearRegion(CacheJob objCacheJob)
        {
            var endpoints = Redis.GetEndPoints(true);
            foreach (var endpoint in endpoints)
            {
                var server = Redis.GetServer(endpoint);
                server.FlushAllDatabases();
            }
        }

        public override void DeleteRegion(CacheJob objCacheJob)
        {
            ClearRegion(objCacheJob);
        }


        private Lock _Lock;

        private Boolean GetLock(string lockKey, TimeSpan timeOut)
        {
            return Redlock.Lock(lockKey, timeOut, out _Lock);
        }

        private void ReleaseLock()
        {
            if (_Lock != null)
            {
                Redlock.Unlock(_Lock);
                _Lock = null;
            }
        }
    }
}